package google.code.asoap;

import google.code.asoap.annotation.SOAPObject;
import google.code.asoap.annotation.SOAPProperty;
import google.code.asoap.annotation.SOAPProperty.Type;
import google.code.asoap.util.Log;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.kobjects.base64.Base64;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapPrimitive;

/**
 * Helper methods for handling Soap operations.
 * 
 * @author Bruno Silva
 */
public class SoapMapper {
    
	public static final String[] empty = new String[0]; 
	
	/**
	 * Parses a SoapObject from a HashMap into a concrete DTO by matching the
	 * DTO field names with the SoapObject's Key and assigning the SoapObject's
	 * value into them.
	 * 
	 * @param target
	 *            the DTO to have the data inserted into
	 * @param hashMapObject
	 *            the SoapObject to parse into the VO
	 */
	public void hashMapToVO(Object target, SoapObject hashMapObject) {
		String key = null;

		for (int i = 0; i < hashMapObject.getPropertyCount(); i++) {
			
			SoapObject property = (SoapObject) hashMapObject.getProperty(i);
			SoapObject propertyKey = (SoapObject) property.getProperty("Key");
			key = propertyKey.getProperty("string").toString();

			try {
				Field field = target.getClass().getDeclaredField(key);
				
				SoapObject propertyValue = (SoapObject) property.getProperty("Value");
				String value = propertyValue.getProperty(0).toString();

				field.setAccessible(true);
				field.set(target, value);
			} catch (NoSuchFieldException e) {
				Log.w("ParseHashMap", "Field not found: " + target.getClass().getSimpleName() + "." + key);
			} catch (IllegalAccessException e) {
				Log.w("ParseHashMap", "Field not acessible: "  + target.getClass().getSimpleName() + "." + key);
			}
		}
	}

	/**
	 * Parses an KSoap2 SoapObject into an annotated VO.
	 * 
	 * @param <T>
	 *            the object type to be created and populated
	 * @param klass
	 *            the class of the target VO
	 * @param soapObject
	 *            the SoapObject with the data for being parsed
	 * @return the created and populated VO of the given class
	 * @throws IllegalArgumentException
	 *             if the provided class isn't annotated by the SOAPObject; if
	 *             the class doesn't provide a public no-args constructor
	 */
	public <T> T parseSoapObjectIntoAnnotatedVO(Class<T> klass, SoapObject soapObject) throws IllegalArgumentException {
		T target;
		try {
			target = klass.newInstance();
		} catch (Exception e) {
			throw new IllegalArgumentException(klass.getSimpleName() + " does not provide public no-args constructor", e);
		}
		SOAPObject objectAnnotation = target.getClass().getAnnotation(SOAPObject.class);
		if (objectAnnotation == null) {
			throw new IllegalArgumentException(target.getClass().getSimpleName() + " doesn't have a SOAPObject annotation.");
		}
		for (Field field : getFieldsByType(target.getClass(), new HashSet<Field>())) {
			try {
				SOAPProperty propertyAnnotation = field.getAnnotation(SOAPProperty.class);
				if (propertyAnnotation != null) {
					String propertyName = propertyAnnotation.name();
					Type propertyType = propertyAnnotation.type();
					if ("".equals(propertyName)) {
						propertyName = field.getName();
					}
					// check if the property is null
					Object objectPropertyValue = soapObject.getProperty(propertyName);
					if (objectPropertyValue == null) {
						continue;
					}
					Object rawValue = null;
					Class<?> fieldType = field.getType();
					if (propertyType == Type.Primitive) {
						if (SoapObject.class.isInstance(objectPropertyValue)) {
							SoapObject propertyValueAsSoapObject = (SoapObject)objectPropertyValue;
							try {
								if (propertyValueAsSoapObject.getPropertyCount() == 0) {
									rawValue = fieldType.newInstance();
								} else {
									rawValue = propertyValueAsSoapObject.getProperty(0);
								}
							} catch (Exception e) {
								//ignore value and move to the next field class
								continue;
							}
						} else {
							rawValue = parseSoapPrimitive((SoapPrimitive) objectPropertyValue, fieldType);
						}
					} else if (propertyType == Type.Complex) {
						SoapObject propertyValue = (SoapObject) objectPropertyValue;
						rawValue = parseSoapObjectIntoAnnotatedVO(fieldType, propertyValue); 
					} else if (propertyType == Type.ComplexArray) {
						if (fieldType.isArray()) {
							SoapObject propertyValue = (SoapObject) objectPropertyValue;
							int elementCount = propertyValue.getPropertyCount();
							rawValue = Array.newInstance(fieldType.getComponentType(), elementCount);
							for (int i = 0; i < elementCount; ++i) {
								Array.set(rawValue, i, parseSoapObjectIntoAnnotatedVO(fieldType.getComponentType(), (SoapObject) propertyValue.getProperty(i)));
							}
						}
					} else if (propertyType == Type.PrimitiveArray) {
						SoapPrimitive propertyValue = (SoapPrimitive) objectPropertyValue;
						if (fieldType == byte[].class) {
							// decode byte[] data from Base64 string
							rawValue = Base64.decode(propertyValue.toString());
						} else {
							throw new RuntimeException("Feature not yet implemented: cannot parse primitive array other than byte[] type.");
						}
					}
					field.setAccessible(true);
					field.set(target, rawValue);
				}
			} catch (IllegalAccessException e) {
				// Note: this piece of code is probably never reached 
				// as we chance accessibility before setting the field value
				Log.w("parseSoapObjectToAnnotatedDTO", "Field not acessible: "  + target.getClass().getSimpleName() + "." + field.getName());
			}
		}
		return target;
	}
	
	private <T> Set<Field> getFieldsByType(Class<T> targetClassType, Set<Field> fields) {
		fields.addAll(Arrays.asList(targetClassType.getDeclaredFields()));
		Class<?> superClassType = targetClassType.getSuperclass();
		if (superClassType.isAnnotationPresent(SOAPObject.class)) {
			fields.addAll(getFieldsByType(superClassType, fields));
		}
		return fields;
	}
	
	private Object parseSoapPrimitive(SoapPrimitive soapPrimitive, Class<?> fieldType) {
		Object rawValue = null;
		if (fieldType == String.class) {
			rawValue = soapPrimitive.toString();
		} else if (fieldType == Boolean.class || fieldType == boolean.class) {
			rawValue = Boolean.parseBoolean(soapPrimitive.toString());
		} else if (fieldType == Character.class || fieldType == char.class) {
			rawValue = soapPrimitive.toString().charAt(0);
		} else if (fieldType == Short.class || fieldType == short.class) {
			rawValue = Short.parseShort(soapPrimitive.toString());
		} else if (fieldType == Integer.class || fieldType == int.class) {
			rawValue = Integer.parseInt(soapPrimitive.toString());
		} else if (fieldType == Long.class || fieldType == long.class) {
			rawValue = Long.parseLong(soapPrimitive.toString());
		} else if (fieldType == Float.class || fieldType == float.class) {
			rawValue = Float.parseFloat(soapPrimitive.toString());
		} else if (fieldType == Double.class  || fieldType == double.class) {
			rawValue = Double.parseDouble(soapPrimitive.toString());
		}
		return rawValue;
	}
}
